/*
 * Copyright (c) 2024. hallele, Inc All rights reserved.
 */

package cn.hallele.ms.infrastructure.auditlog.aspect;

import cn.hallele.infrastructure.message.io.Message;
import cn.hallele.infrastructure.message.util.MqUtil;
import cn.hallele.infrastructure.metadata.constant.AppConst;
import cn.hallele.infrastructure.metadata.constant.MsgCodeConst;
import cn.hallele.infrastructure.metadata.constant.SymbolConstant;
import cn.hallele.infrastructure.metadata.exception.HalException;
import cn.hallele.infrastructure.metadata.io.Request;
import cn.hallele.infrastructure.util.DateUtil;
import cn.hallele.infrastructure.util.NetUtil;
import cn.hallele.infrastructure.util.ObjectUtil;
import cn.hallele.ms.infrastructure.auditlog.annotation.ApiLog;
import cn.hallele.ms.infrastructure.auditlog.config.AuditLogProperties;
import cn.hallele.ms.infrastructure.auditlog.io.ApiLogIO;
import jakarta.servlet.http.HttpServletRequest;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.slf4j.MDC;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.MessageSource;
import org.springframework.context.i18n.LocaleContextHolder;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;

import java.util.*;

/**
 * api日志的拦截器
 *
 * @author anle5
 * @since 2024/9/14 22:39
 */
@Slf4j
@Aspect
@Order(20)
@Component
public class ApiLogAspect {
    @Value("${spring.application.name:Unknown}")
    private String applicationName;

    @Autowired
    private AuditLogProperties properties;

    @Autowired
    private MessageSource i18n;

    @Pointcut("@annotation(cn.hallele.ms.infrastructure.auditlog.annotation.ApiLog)")
    private void pointcut() {
    }

    @Around("pointcut()")
    public Object around(ProceedingJoinPoint point) throws Throwable {
        ApiLogIO logIO = ApiLogIO.builder().build();
        try {
            buildBasicInfo(logIO);
            buildRequest(logIO, point);
            buildInitiator(logIO, point);

            logIO.setTranceId(MDC.get(AppConst.TRANCE_ID));
            MethodSignature methodSignature = (MethodSignature) point.getSignature();
            logIO.setApiId(methodSignature.getMethod().getAnnotation(ApiLog.class).id());

            // 时间、响应
            logIO.setStartTime(DateUtil.nowDate());
            Object response = point.proceed();
            logIO.setResponse(canToJson(response) ? ObjectUtil.toJsonString(response)
                    : ObjectUtil.map(response, r -> response.getClass().getName()));
            logIO.setMsgCode(MsgCodeConst.SUCCESS);
            logIO.setMsgDesc(i18n.getMessage(MsgCodeConst.SUCCESS, new Object[]{}, LocaleContextHolder.getLocale()));
            return response;
        } catch (Exception exception) {
            String msgCode;
            String msgDesc;
            if (exception instanceof HalException halException) {
                msgCode = halException.getMessage();
                msgDesc = i18n.getMessage(msgCode, halException.getArgs(), LocaleContextHolder.getLocale());
            } else {
                msgCode = MsgCodeConst.FAILED;
                msgDesc = i18n.getMessage(msgCode, new Object[]{exception.getMessage()}, LocaleContextHolder.getLocale());
            }
            logIO.setMsgCode(msgCode);
            logIO.setMsgDesc(msgDesc);
            throw exception;
        } finally {
            logIO.setEndTime(DateUtil.nowDate());
            boolean successful = MqUtil.sendMsg(Message.builder()
                    .sessionId(MqUtil.genSessionId())
                    .bindingName(properties.getBindingName())
                    .message(logIO)
                    .build());
            if (!successful) {
                // todo: 告警
            }
        }
    }

    private void buildInitiator(ApiLogIO logIO, ProceedingJoinPoint point) {
        for (Object arg : point.getArgs()) {
            if (arg instanceof Request<?> request && request.getInitiator() != null) {
                // 说明是业务微服务
                logIO.setInitiator(request.getInitiator().getId());
                return;
            }
        }

        // 说明是统一接入微服务
        if (NetUtil.getLoginUser() != null) {
            logIO.setInitiator(NetUtil.getLoginUser().getId());
        }
    }

    private void buildRequest(ApiLogIO logIO, ProceedingJoinPoint point) {
        // 拼接报文
        HttpServletRequest request = NetUtil.getHttpServletRequest();
        StringJoiner requestBody = new StringJoiner(System.lineSeparator());
        Object[] args = point.getArgs();
        requestBody.add(request.getMethod() + SymbolConstant.SPACE + request.getRequestURI())
                .add(getHeader(request))
                .add(SymbolConstant.EMPTY);
        Map<String, Object> body = new HashMap<>();
        String[] parameterNames = ((MethodSignature) point.getSignature()).getParameterNames();
        for (int i = 0; i < parameterNames.length; i++) {
            Object arg = args[i];
            if (arg == null) {
                continue;
            }

            if (canToJson(arg)) {
                body.put(parameterNames[i], arg);
            } else {
                body.put(arg.getClass().getSimpleName(), arg.getClass().getName());
            }
        }
        requestBody.add(ObjectUtil.toJsonString(body));

        logIO.setRequest(requestBody.toString());
    }

    private void buildBasicInfo(ApiLogIO logIO) {
        logIO.setAppName(this.applicationName);
        logIO.setIp(NetUtil.getClientIp());
        logIO.setClientInfo(NetUtil.getClientStringInfo());
    }

    private String getHeader(HttpServletRequest request) {
        Enumeration<String> headerNames = request.getHeaderNames();
        StringJoiner joiner = new StringJoiner(System.lineSeparator());
        while (headerNames.hasMoreElements()) {
            String headerName = headerNames.nextElement();
            String header = request.getHeader(headerName);
            joiner.add(headerName + SymbolConstant.COLON + SymbolConstant.EMPTY + header);
        }
        return joiner.toString();
    }

    /**
     * 是否可以被序列化成json，但是针对泛型不是万能的
     *
     * @param object json序列化对象
     * @return true 可以，反之不可以
     */
    private boolean canToJson(Object object) {
        if (object == null) {
            return false;
        }
        List<String> whiteListClass = properties.getSerializeWhitelistClass();
        List<String> blackListClass = properties.getSerializeBlacklistClass();

        return (whiteListClass.contains(object.getClass().getName()) || whiteListClass.contains(object.getClass().getPackageName()))
                && !(blackListClass.contains(object.getClass().getName()) || blackListClass.contains(object.getClass().getPackageName()));
    }
}
